package ro.one.gaeql.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import ro.one.gaeql.common.JobStates;
import ro.one.gaeql.model.AssignExpression;
import ro.one.gaeql.model.Assignment;
import ro.one.gaeql.model.Condition;
import ro.one.gaeql.model.Kind;
import ro.one.gaeql.model.Property;
import ro.one.gaeql.model.UpdateQuery;
import ro.one.gaeql.model.Using;
import ro.one.gaeql.model.Where;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.taskqueue.DeferredTask;

public class UpdateQueryTask implements DeferredTask {
	private static final long serialVersionUID = 1L;

	private UpdateQuery uq;
	private String idJob;
	private Instrumentation instrumentation;
	
	public UpdateQueryTask(UpdateQuery uq, String idJob) {
		this.uq = uq;
		this.idJob = idJob;
	}

	public void setInstrumentation(Instrumentation instrumentation) {
		this.instrumentation = instrumentation;
	}
	
	private Iterable<Entity> getEntities(Kind kind, Where where, Entity inputEntity) {
		Iterable<Entity> entities;
		DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
		if(!this.isQueryByKey(where)) {
			
			DatastoreQueryBuilder builder = new DatastoreQueryBuilder(kind.getName());
			
			this.addConditions(where, builder, inputEntity);
			
			Query q = builder.createQuery();
			instrQueryBuilt(q);
			PreparedQuery pq = ds.prepare(q);
			entities = pq.asIterable();
			
		} else {
			Key k = this.getKey(kind, where);
			try {
				Entity e = ds.get(k);
				List<Entity> list = new ArrayList<Entity>();
				list.add(e);
				entities = list;
			} catch(Exception ex) {
				throw new RuntimeException(ex);
			}
		}
		return entities;
	}
	
	@Override
	public void run() {
		try {
			for (Entity result : this.getEntities(this.uq.getKind(), this.uq.getWhere(), null)) {
				Map<Kind, Entity> usingResults = new HashMap<Kind, Entity>();
				usingResults.put(uq.getKind(), result);

				for (Using using : this.uq.getUsings()) {
					Entity usingResult = this.getUsingResult(result, using);
					usingResults.put(using.getKind(), usingResult);
				}

				for (Assignment assignment : uq.getAssignments()) {
					Property property = assignment.getProperty();

					Kind propertyKind = Kind.getKindByAlias(
							property.getAlias(), uq.getAliases(), uq.getKind());

					if (propertyKind != uq.getKind()) {
						throw new RuntimeException(
								"Cannot assign values on properties from other kinds.");
					}

					String propertyName = property.getName();
					AssignExpression expression = assignment.getExpression();

					Object value = expression.eval(new EvalContextImpl(usingResults, uq.getAliases()));
					
					instrSettingProperty(result, propertyName, value);
					result.setProperty(propertyName, value);
				}

				DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
				ds.put(result);
			}

			JobEntity job = JobEntity.get(idJob);
			job.setState(JobStates.DONE);
			job.save();
		} catch (Exception e) {
			JobEntity job = JobEntity.get(idJob);
			job.setState(JobStates.FAILED);
			job.save();
		}
	}

	private Key getKey(Kind kind, Where where) {
		while(where != null) {
			if(this.isKey(where)) {
				Object expression = where.getLeftCondition().getExpression();
				if(expression instanceof Integer) {
					return KeyFactory.createKey(kind.getName(), new Long((Integer)expression));
				} else if(expression instanceof String) {
					return KeyFactory.createKey(kind.getName(), (String)expression);
				}
			}
			
		}
		return null;
	}
	
	private boolean isKey(Where where) {
		return (where.getLeftCondition().getProperty() == null && where.getRightWhere() == null);
	}

	private boolean isQueryByKey(Where where) {
		while(where != null) {
			if(this.isKey(where))
				return true;
			where = where.getRightWhere();
		}
		return false;
	}

	private Entity getUsingResult(Entity input, Using u) {
		Integer count = 0;

		Entity result = null;

		for (Entity e : this.getEntities(u.getKind(), u.getWhere(), input)) {
			if (count > 1)
				throw new RuntimeException("Cannot have more than one result on a using clause.");
			result = e;
			count++;
		}

		if (result == null)
			throw new RuntimeException("Using clause on entity '"
					+ u.getKind().getName() + "' returned no result.");

		return result;
	}

	private void addConditions(Where where, DatastoreQueryBuilder builder, Entity inputEntity) {
		while (where != null) {
			Condition c = where.getLeftCondition();
			Object expression = c.getExpression();
			
			Object value = expression;
			
			if (inputEntity != null) {
				if (expression instanceof Property) {
					Property property = (Property) expression;
					value = inputEntity.getProperty(property.getName());
				} else {
					value = expression;
				}
			}
			
			builder.addCondition(c.getProperty().getName(), c.getOperator(), value);			
			where = where.getRightWhere();
		}
	}
	
	private void instrSettingProperty(Entity result, String propertyName, Object value) {
		if (instrumentation == null)
			return;
		
		instrumentation.settingProperty(result, propertyName, value);
	}

	private void instrQueryBuilt(Query q) {
		if (instrumentation == null)
			return;
		
		instrumentation.datastoreQueryBuilt(q);
	}
}
